package gogpc

import (
	"fmt"
	"math"
	"os"

	"gitee.com/tfcolin/dsg"
	"gitee.com/tfcolin/gomat"
)

type Face struct {
	ori   Point
	coor_ []float64
	coor  [][]float64
}

type Area struct {
	*Face
	*Poly
	is_check bool
}

type Scene struct {
	area         [](*Area)
	norm_ws      []float64
	mat_ws, b_ws []float64
}

func InitFace() *Face {
	var f Face
	f.ori = make(Point, 3)
	f.coor_ = make([]float64, 9)
	f.coor = dsg.IndexArray2D(f.coor_, []int{3, 3})
	return &f
}

func InitArea() *Area {
	var area Area
	area.Face = InitFace()
	area.Poly = InitPoly()
	return &area
}

func InitScene() *Scene {
	return &Scene{area: make([]*Area, 0)}
}

func (s *Scene) NewArea() *Area {
	new_area := InitArea()
	s.area = append(s.area, new_area)
	return new_area
}

func (s *Scene) GetTotalNPoint() (tnp, mnp int) {
	for _, area := range s.area {
		np := area.GetTotalNPoint()
		tnp += np
		if np > mnp {
			mnp = np
		}
	}
	return
}

func InnerProd(p1 []float64, p2 []float64) (res float64) {
	for i := 0; i < len(p1); i++ {
		res += p1[i] * p2[i]
	}
	return
}

func Mod(p1 []float64) float64 {
	return math.Sqrt(InnerProd(p1, p1))
}

func Unify(v []float64) float64 {
	m := math.Sqrt(InnerProd(v, v))
	for i := 0; i < len(v); i++ {
		v[i] /= m
	}
	return m
}

func CrossProd(v1 []float64, v2 []float64, res []float64) {
	for i := 0; i < 3; i++ {
		i1 := (i + 1) % 3
		i2 := (i + 2) % 3
		res[i] = v1[i1]*v2[i2] - v1[i2]*v2[i1]
	}
	return
}

func Normalize(coor []float64, new_coor []float64, trans_mat []float64) {
	copy(new_coor[0:3], coor[0:3])
	m1 := Unify(new_coor[0:3])
	x2e1 := InnerProd(new_coor[0:3], coor[3:6])
	for i := 0; i < 3; i++ {
		new_coor[3+i] = coor[3+i] - x2e1*new_coor[i]
	}
	m2 := Unify(new_coor[3:6])
	CrossProd(new_coor[0:3], new_coor[3:6], new_coor[6:9])

	for i := 0; i < 9; i++ {
		trans_mat[i] = 0
	}
	trans_mat[0] = m1
	trans_mat[1] = x2e1
	trans_mat[4] = m2
	trans_mat[8] = 1

	copy(coor, new_coor[:9])
}

// size of ws = 9 * 2 + tnp * 3
func (a *Area) Normalize(ws []float64) {
	tnp := a.GetTotalNPoint()
	Normalize(a.coor_, ws[0:9], ws[9:18])
	gomat.MatMat(3, tnp, 3, ws[9:18], a.ws, ws[18:18+tnp*3])
	copy(a.ws, ws[18:18+tnp*3])
}

func (s *Scene) Finish (need_norm bool) {
	_, mnp := s.GetTotalNPoint()
	s.norm_ws = make([]float64, 9*2+mnp*3)
	s.mat_ws = s.norm_ws[0:9]
	s.b_ws = make([]float64, 3)

	if need_norm {
		for _, area := range s.area {
			area.Normalize(s.norm_ws)
		}
	}
}

func ReadScene(fout_name string) (s *Scene) {
	s = InitScene()
	fout, err := os.Open(fout_name)
	if err != nil {
		panic("cannot open file " + fout_name)
	}

	var narea int
	fmt.Fscan(fout, &narea)

	for i := 0; i < narea; i++ {
		area := s.NewArea()
		for j := 0; j < 3; j++ {
			fmt.Fscan(fout, &area.ori[j])
		}
		for j := 0; j < 3; j++ {
			fmt.Fscan(fout, &area.coor[0][j])
		}
		for j := 0; j < 3; j++ {
			fmt.Fscan(fout, &area.coor[1][j])
		}
		fmt.Fscan(fout, &area.is_check)
		var nbound, npoint int
		fmt.Fscan(fout, &nbound)
		for j := 0; j < nbound; j++ {
			area.NewBound(false)
			fmt.Fscan(fout, &npoint)
			for k := 0; k < npoint; k++ {
				var x, y float64
				fmt.Fscan(fout, &x, &y)
				area.NewPoint(x, y)
			}
		}
		area.Finish()
	}

	s.Finish(true)
	return s
}

func plane_inter_z(f *Face, qp []float64, mat_ws []float64, b_ws []float64) float64 {
	copy(mat_ws[0:3], f.coor[0])
	copy(mat_ws[3:6], f.coor[1])
	mat_ws[6] = 0
	mat_ws[7] = 0
	mat_ws[8] = -1
	copy(b_ws, qp[0:2])
	b_ws[2] = 0

	for i := 0; i < 3; i++ {
		b_ws[i] -= f.ori[i]
	}
	gomat.SolveLinearEquations(3, mat_ws, 1, b_ws)
	return b_ws[2]
}

func compare_area(a1 *Area, a2 *Area, mat_ws, b_ws []float64) int {
	a1.SetPoly(0)
	a2.SetPoly(1)
	PolyInter(0, 1, 2)
	a3p := GetPoly(2)
	if len(a3p.bound) == 0 {
		return 0
	}

	a3p_np := a3p.GetTotalNPoint()
	var z1, z2 float64
	for i := 0; i < a3p_np; i++ {
		z1 = plane_inter_z(a1.Face, a3p.ws[i*3:i*3+2], mat_ws, b_ws)
		z2 = plane_inter_z(a2.Face, a3p.ws[i*3:i*3+2], mat_ws, b_ws)
		if math.Abs (z1 - z2) > 10 * EPS {break}
	}

	if z1 < z2 {
		return -1
	} else if z1 > z2 {
		return 1
	} else {
		return 0
	}
}

func (a *Area) TransMap(trans_coor []float64) *Area {
	ta := InitArea()
	gomat.MatTransMat(3, 3, 3, trans_coor, a.coor_, ta.coor_)
	gomat.MatTransVec(3, trans_coor, a.ori, ta.ori)
	ta.is_check = a.is_check

	if math.Abs(ta.coor[2][2]) < EPS {
		return nil
	}

	for _, bound := range a.bound {
		ta.NewBound(bound.is_hole)
		for range bound.point {
			ta.NewPoint(0, 0)
		}
	}
	ta.Finish()

	tnp := a.GetTotalNPoint()
	gomat.MatMat(3, tnp, 3, ta.coor_, a.ws, ta.ws)

	for i := 0; i < tnp; i++ {
		for j := 0; j < 3; j++ {
			ta.ws[i*3+j] += ta.ori[j]
		}
	}

	return ta
}

func make_dag_by_areas(proj_area [](*Area), mat_ws, b_ws []float64) *dsg.DAG {
	narea := len(proj_area)
	dag := dsg.InitDAG(narea)

	for i := 0; i < narea; i++ {
		if proj_area[i] == nil {
			continue
		}
		bb1min, bb1max := proj_area[i].CalBoundingBox()
		for j := 0; j < narea; j++ {
			bb2min, bb2max := proj_area[j].CalBoundingBox()
			is_bbinter := true
			for i := 0; i < 2; i++ {
				if bb1min[i] > bb2max[i] || bb2min[i] > bb1max[i] {
					is_bbinter = false
					break
				}
			}
			if !is_bbinter {
				continue
			}
			comp := compare_area(proj_area[i], proj_area[j], mat_ws, b_ws)
			if comp < 0 {
				dag.AddLink(j, i)
			}
			if comp > 0 {
				dag.AddLink(i, j)
			}
		}
	}

	return dag
}

// draw_classes [4]: 0: normal 1: hidden 2: checked 3: hidden and checked
func (s *Scene) Draw (proj_coor []float64, d Drawer, draw_classes []int) {
	narea := len(s.area)
	proj_area := make([](*Area), narea)

	for i, area := range s.area {
		proj_area[i] = area.TransMap(proj_coor)
	}

	dag := make_dag_by_areas(proj_area, s.mat_ws, s.b_ws)

	c_union := InitPoly()
	c_union.Finish()

	for {
		ia := dag.PopHead()
		if ia == -1 {
			break
		}
		area := proj_area[ia]
		if area == nil {
			continue
		}
		area.SetPoly(0)
		c_union.SetPoly(1)
		PolyDiff(0, 1, 2)
		PolyUnion(0, 1, 3)
		PolyInter(0, 1, 4)
		c_union = GetPoly(3)
		diff := GetPoly(2)
		hide := GetPoly(4)
		var pens []int
		if area.is_check {
			pens = draw_classes[2:4]
		} else {
			pens = draw_classes[0:2]
		}
		diff.Draw(d, pens[0])
		hide.Draw(d, pens[1])
	}
}

func (s *Scene) CheckProjection(proj_dir []float64, window *Area, is_debug bool) (res bool, shadow_scene * Scene) {
	var shadow_area * Area

	if (is_debug) {
		shadow_scene = InitScene()
	}

	for i := 0; i < 3; i ++ {
		proj_dir[i] = - proj_dir[i]
	}

	proj_coor := make([]float64, 9)
	copy(proj_coor[6:9], proj_dir[0:3])
	Unify(proj_coor[6:9])
	if proj_coor[6] == 0 && proj_coor[7] == 0 {
		proj_coor[0] = 1
		proj_coor[4] = 1
	} else {
		proj_coor[0] = -proj_coor[7]
		proj_coor[1] = proj_coor[6]
		proj_coor[2] = 0
		Unify(proj_coor[0:3])
		CrossProd(proj_coor[6:9], proj_coor[0:3], proj_coor[3:6])
	}

	proj_window := window.TransMap(proj_coor)
	if proj_window == nil {
		return false, nil
	}

	narea := len(s.area)
	proj_area := make([](*Area), narea)
	for i, area := range s.area {
		proj_area[i] = area.TransMap(proj_coor)
	}
	dag := make_dag_by_areas(proj_area, s.mat_ws, s.b_ws)

	bbmin, bbmax := proj_window.CalBoundingBox()

	c_rest := proj_window.Poly
	for {
		ia := dag.PopHead()
		if ia == -1 {
			break
		}
		area := proj_area[ia]
		if area == nil {
			continue
		}

		abbmin, abbmax := area.CalBoundingBox()
		is_bbinter := true
		for i := 0; i < 2; i++ {
			if abbmin[i] > bbmax[i] || bbmin[i] > abbmax[i] {
				is_bbinter = false
				break
			}
		}
		if !is_bbinter {
			continue
		}

		c_rest.SetPoly(0)
		area.SetPoly(1)
		PolyDiff(0, 1, 2)
		PolyInter(0, 1, 3)
		c_rest = GetPoly(2)
		inter := GetPoly(3)

		if !inter.IsEmpty() {
			if is_debug {
				shadow_area = shadow_scene.NewArea()
				shadow_area.is_check = area.is_check
				for _, bound := range inter.bound {
					shadow_area.NewBound (bound.is_hole)
					for _, point := range bound.point {
						shadow_area.NewPoint (point[0], point[1])
					}
				}
				shadow_area.Finish()
				tnp := inter.GetTotalNPoint()

				for i := 0; i < tnp; i ++ {
					shadow_area.ws[i * 3 + 2] = plane_inter_z (area.Face, shadow_area.ws[i * 3 : i * 3 + 2], s.mat_ws, s.b_ws)
				}

				copy(shadow_area.ori, s.area[ia].ori)
				copy(shadow_area.coor_, s.area[ia].coor_)
				gomat.MatMat (3, tnp, 3, proj_coor, shadow_area.ws, inter.ws)
				for i := 0; i < tnp; i ++ {
					for j := 0; j < 3; j ++ {
						inter.ws[i * 3 + j] -= shadow_area.ori[j]
					}
				}
				gomat.MatTransMat (3, tnp, 3, shadow_area.coor_, inter.ws, shadow_area.ws)
			}

			if area.is_check {
				res = true
				if (!is_debug) {break}
			}
		}
	}

	if is_debug {
		shadow_scene.Finish (false)
	}

	return
}
